﻿using UnityEngine;
using System;
using System.Threading;
using System.Timers;
using System.Collections;
using System.Net.Sockets;
using System.Net;
using System.Collections.Generic;
using Timer = System.Timers.Timer;

public class Server : MonoBehaviour {
	public bool isPrimary = false;
	private UdpClient udp;
	private Thread discover;
	private Timer broadcast;

	private TcpClient tcp;
	private TcpListener listener;
	private Thread connected;
	private Thread internalCommands;
	private int port = 2004;

	private short nextPlayerNumber = 0;

	/* HOST */
	private Queue<Socket> joiningPlayers = new Queue<Socket>();
	private Dictionary<Socket, short> connectedPlayers = new Dictionary<Socket, short>();

	/* CLIENT */
	private Dictionary<short, RemotePlayer> remotePlayers = new Dictionary<short, RemotePlayer>();
	private Queue<short> joiningRemotePlayers = new Queue<short>();
	public short me;

	public event Action<Socket> ConnectionEstablished;
	public event Action<RemotePlayer, RemotePlayer> someoneDied; // killer, victim
	public GameObject playerPrefab;


	private const short INTERNAL_NEWPLAYERS = 0;
	private const short INTERNAL_POSITION = 1;
	private const short INTERNAL_DEAD = 2;

	// Use this for initialization
	void Start () {
		DontDestroyOnLoad(this);
		Debug.Log (LocalIPAddress());

		udp = new UdpClient();
		udp.EnableBroadcast = true;
		discover = new Thread(Discover);
		discover.Start();

		tcp = new TcpClient();
	}

	void OnDestroy() {
		Dispose ();
	}

	void Update() {
		while(joiningPlayers.Count > 0) {
			var socket = joiningPlayers.Dequeue();
			var newPlayer = (GameObject)Instantiate(playerPrefab, Vector2.zero, new Quaternion());
			newPlayer.GetComponent<RemotePlayer>().StartRemoteControl(socket);
		}

		while(joiningRemotePlayers.Count > 0) {
			var number = joiningRemotePlayers.Dequeue();
			var newRemotePlayer = (GameObject)Instantiate (playerPrefab, Vector2.zero, new Quaternion());
			var remote = newRemotePlayer.GetComponent<RemotePlayer>();
			remotePlayers.Add(number, remote);
		}
	}

	/* REMOTE CLIENT */

	public void JoinHost(IPAddress remote) {
		Debug.Log ("Joining host " + remote);

		tcp.Connect(remote, port);
		//ConnectionEstablished(tcp.Client);
		internalCommands = new Thread(InternalCommands);
		internalCommands.Start();
	}

	public Socket GetSocket() {
		return tcp.Client;
	}

	public short GetPlayerNumber(RemotePlayer rm) {
		foreach(var rmp in remotePlayers)
			if(rmp.Value == rm) return rmp.Key;
		return -1;
	}

	private void InternalCommands() {
		while(internalCommands.IsAlive) {
			Debug.Log ("Waiting for internal command");
			var command = tcp.Client.ReadShort();

			Debug.Log ("Internal command " + command + " just got in");
			switch (command) {
			case INTERNAL_NEWPLAYERS:
				handleNewRemotePlayers();
				break;
			case INTERNAL_POSITION:
				handleNewPosition();
				break;
			case INTERNAL_DEAD:
				handleDeath();
				break;
			default:
				Debug.Log ("Something internal wong! Now wong!!");
				break;
			}
		}
	}

	private void handleDeath() {
		var playerNumberDead = tcp.Client.ReadShort();
		var playerNumberKiller = tcp.Client.ReadShort();

		Debug.Log (playerNumberKiller + " Killed " + playerNumberDead + " . I'm " + me);

		var gMe = GameObject.Find("NinjaWrap").GetComponent<RemotePlayer>();

		someoneDied(playerNumberKiller == me ? gMe : remotePlayers[playerNumberKiller],
		            playerNumberDead == me ? gMe : remotePlayers[playerNumberDead]);

		if(playerNumberDead == me) 
			GameObject.Find("NinjaWrap").GetComponent<RemotePlayer>().Die();
		else
			remotePlayers[playerNumberDead].DisplayDeath();
	}

	private void handleNewPosition() {
		Debug.Log ("Updating positions");
		var playerNumber = tcp.Client.ReadShort();
		var x = tcp.Client.ReadFloat();
		var y = tcp.Client.ReadFloat();

		Debug.Log ("Updating player " + playerNumber + " position");

		if(remotePlayers.ContainsKey(playerNumber))
			remotePlayers[playerNumber].CreatePositionCommand(x,y);
		else
			Debug.Log("Player " + playerNumber + " has not joined this game fully yet");
	}

	private void handleNewRemotePlayers() {
		Debug.Log ("Getting new remote player");
		var l = tcp.Client.ReadShort();
		me = tcp.Client.ReadShort();
		Debug.Log ("I'm player " + me);
		Debug.Log ("There are " + l + " players in the game now");
		for(var i = 0; i < l; i++) {
			var number = tcp.Client.ReadShort();
			if(me != number && !remotePlayers.ContainsKey(number)) {
				Debug.Log ("That player was : " + number);
				joiningRemotePlayers.Enqueue(number);
			}
		}
	}

	private void Discover() {
		while(discover.IsAlive) {
			var point = new IPEndPoint(IPAddress.Any, 3000);
			try {
				Debug.Log ("Listening for desicovery");
				var bytes = udp.Receive(ref point);
				Debug.Log ("Heard broadcast");

				var ip = bytes.GetString();
				var remote = IPAddress.Parse(ip);

				JoinHost(remote);
			} catch(Exception e) {
				Debug.Log("Receive got interrupted");
				Debug.Log (e);
			}
		}
	}

	/* HOST CLIENT */

	public void StartHost() {
		discover.Abort();
		isPrimary = true;
		var end = new IPEndPoint(IPAddress.Broadcast, 3000);
		var ip = LocalIPAddress().GetBytes();

		Debug.Log (LocalIPAddress());

		Debug.Log ("Starting broadcast");
		broadcast = new Timer(1000);
		broadcast.Elapsed += (sender, e) => {
			udp.Send(ip, ip.Length, end); 
		};
		broadcast.Start();

		connected = new Thread(Connected);
		connected.Start();
	}

	private void Connected() {
		listener = new TcpListener(IPAddress.Any, port);
		listener.Start();

		while(connected.IsAlive) {
			try {
				Debug.Log("Starting connection listening");
				var socket = listener.AcceptSocket();
				Debug.Log("Connection socket accepted");
				joiningPlayers.Enqueue(socket);
				connectedPlayers.Add(socket, nextPlayerNumber++);
				NotifyAllPlayersConnected();
			} catch(Exception e) {
				Debug.Log("Connection was aborted");
				Debug.Log (e);
			}
		}
	}

	public void PlayerPositionUpdated(Socket socket, byte[] positionData) {
		if(!isPrimary) return;

//		Debug.Log ("Updating every players position");

		var pNumber = connectedPlayers[socket];

		foreach(var pair in connectedPlayers) {
			if(socket == pair.Key || pair.Key == null) continue;
//			Debug.Log ("Updating for player " + pair.Value);
			var bytes = new byte[12];
			BitConverter.GetBytes(INTERNAL_POSITION).CopyTo(bytes, 0);
			BitConverter.GetBytes(pNumber).CopyTo (bytes, 2);
			positionData.CopyTo(bytes, 4);
			pair.Key.Send(bytes);
		} 
		//Debug.Log ("All send");
	}

	public void NotifyAllPlayersConnected() {
		
		var l = (short)connectedPlayers.Count;
		var headbytes = new byte[4];
		BitConverter.GetBytes(INTERNAL_NEWPLAYERS).CopyTo(headbytes, 0);
		BitConverter.GetBytes(l).CopyTo(headbytes, 2);

		var content = new byte[l*2];
		int index = 0;
		foreach(var p in connectedPlayers) {
			BitConverter.GetBytes(p.Value).CopyTo(content, index);
			index += 2;
			Debug.Log("creating stuff");
		}
		foreach(var p in connectedPlayers) {
			var bs = new byte[headbytes.Length + content.Length + 2];
			headbytes.CopyTo(bs, 0);
			BitConverter.GetBytes(p.Value).CopyTo(bs, headbytes.Length);
			content.CopyTo(bs, headbytes.Length + 2);
			Debug.Log ("We send " + p.Key.Send(bs) + " bytes");
		}
		Debug.Log ("Notification to " + l + " players send");
	}

	public void PlayerDead(Socket s, short victim, short killer) {
		var bytes = new byte[6];
		BitConverter.GetBytes(INTERNAL_DEAD).CopyTo(bytes, 0);
		BitConverter.GetBytes(victim).CopyTo(bytes, 2);
		BitConverter.GetBytes(killer).CopyTo(bytes, 4);

		foreach(var pair in connectedPlayers) {
			pair.Key.Send(bytes);
		}
	}

	/* HELPERS */

	public string LocalIPAddress()
	{
		IPHostEntry host;
		string localIP = "";
		host = Dns.GetHostEntry(Dns.GetHostName());
		foreach (IPAddress ip in host.AddressList)
		{
			if (ip.AddressFamily == AddressFamily.InterNetwork)
			{
				localIP = ip.ToString();
				break;
			}
		}
		return localIP;
	}

	public void Dispose() {
		if(broadcast != null) broadcast.Stop();
		if(connected != null && connected.IsAlive) connected.Abort();
		if(discover != null && discover.IsAlive) discover.Abort();
		if(internalCommands != null && internalCommands.IsAlive) internalCommands.Abort();

	}
}
